// Protocol Buffers - Google's data interchange format
// Copyright 2008 Google Inc.  All rights reserved.
// http://code.google.com/p/protobuf/
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
//     * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
//     * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

package com.googlecode.protoserv;

import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.math.BigInteger;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import com.google.protobuf.ByteString;
import com.google.protobuf.Descriptors.Descriptor;
import com.google.protobuf.Descriptors.EnumDescriptor;
import com.google.protobuf.Descriptors.EnumValueDescriptor;
import com.google.protobuf.Descriptors.FieldDescriptor;
import com.google.protobuf.ExtensionRegistry;
import com.google.protobuf.Message;
import com.google.protobuf.TextFormat;
import com.google.protobuf.UnknownFieldSet;
import com.google.protobuf.WireFormat;

/**
 * Provide text parsing and formatting support for proto2 instances. The
 * implementation largely follows google/protobuf/text_format.cc.
 * 
 * @author wenboz@google.com Wenbo Zhu
 * @author kenton@google.com Kenton Varda
 */
final class JsonFormat {
  private JsonFormat() {
  }

  private static String getFieldName(FieldDescriptor field) {
    if (field.isExtension()) {
      // We special-case MessageSet elements for compatibility with proto1.
      if (field.getContainingType().getOptions().getMessageSetWireFormat()
          && (field.getType() == FieldDescriptor.Type.MESSAGE)
          && (field.isOptional())
          // object equality
          && (field.getExtensionScope() == field.getMessageType())) {
        return "(" + field.getMessageType().getFullName() + ")";
      } else {
        return "(" + field.getFullName() + ")";
      }
    } else {
      if (field.getType() == FieldDescriptor.Type.GROUP) {
        // Groups must be serialized with their original capitalization.
        return field.getMessageType().getName();
      } else {
        return field.getName();
      }
    }
  }

  private static String unsignedToString(final int value) {
    if (value >= 0) {
      return Integer.toString(value);
    } else {
      return Long.toString(((long) value) & 0x00000000FFFFFFFFL);
    }
  }

  private static String unsignedToString(final long value) {
    if (value >= 0) {
      return Long.toString(value);
    } else {
      // Pull off the most-significant bit so that BigInteger doesn't think
      // the number is negative, then set it again using setBit().
      return BigInteger.valueOf(value & 0x7FFFFFFFFFFFFFFFL).setBit(63)
          .toString();
    }
  }

  private static void printFieldValue(FieldDescriptor.Type type, Object value,
      JsonWriter writer) throws IOException {
    switch (type) {
    case INT32:
    case SINT32:
    case SFIXED32:
      writer.value((Integer) value);
      break;
    case INT64:
    case SINT64:
    case SFIXED64:
      writer.value((Long) value);
      break;
    case FLOAT:
      writer.value((Float) value);
      break;
    case DOUBLE:
      writer.value((Double) value);
      break;
    case BOOL:
      writer.value(value.toString().toLowerCase());
      break;
    case UINT32:
    case FIXED32:
      writer.value(unsignedToString((Integer) value));
      break;
    case UINT64:
    case FIXED64:
      writer.value(unsignedToString((Long) value));
      break;
    case STRING:
      writer.value((String) value);
      break;
    case BYTES:
      writer.value(Base64.encode((ByteString) value));
      break;
    case ENUM:
      writer.value(((EnumValueDescriptor) value).getName());
      break;
    case MESSAGE:
    case GROUP:
      stringify((Message) value, writer);
      break;
    }
  }

  private static void printUnknownFieldValue(int wireType, Object value,
      JsonWriter writer) throws IOException {
    switch (wireType) {
    case WireFormat.WIRETYPE_VARINT:
      writer.value(unsignedToString((Long) value));
      break;
    case WireFormat.WIRETYPE_FIXED32:
    case WireFormat.WIRETYPE_FIXED64:
      writer.value(value.toString());
      break;
    case WireFormat.WIRETYPE_LENGTH_DELIMITED:
      writer.value(Base64.encode((ByteString) value));
      break;
    case WireFormat.WIRETYPE_START_GROUP:
      writer.beginObject();
      printUnknownFieldValues((UnknownFieldSet) value, writer);
      writer.endObject();
      break;
    default:
      throw new IllegalArgumentException("Bad wire type: " + wireType);
    }
  }

  private static void printUnknownFieldValues(int wireType, List<?> values,
      JsonWriter writer) throws IOException {
    for (Object value : values) {
      printUnknownFieldValue(wireType, value, writer);
    }
  }

  private static void printUnknownFieldValues(UnknownFieldSet unknownFields,
      JsonWriter writer) throws IOException {
    for (Map.Entry<Integer, UnknownFieldSet.Field> entry : unknownFields
        .asMap().entrySet()) {
      int number = entry.getKey();
      writer.name(String.valueOf(number));
      writer.beginArray();
      UnknownFieldSet.Field field = entry.getValue();
      printUnknownFieldValues(WireFormat.WIRETYPE_VARINT,
          field.getVarintList(), writer);
      printUnknownFieldValues(WireFormat.WIRETYPE_FIXED32,
          field.getFixed32List(), writer);
      printUnknownFieldValues(WireFormat.WIRETYPE_FIXED64,
          field.getFixed64List(), writer);
      printUnknownFieldValues(WireFormat.WIRETYPE_LENGTH_DELIMITED,
          field.getLengthDelimitedList(), writer);
      printUnknownFieldValues(WireFormat.WIRETYPE_START_GROUP,
          field.getGroupList(), writer);
      writer.endArray();
    }
  }

  public static void stringify(Message message, JsonWriter writer)
      throws IOException {
    writer.beginObject();
    for (Map.Entry<FieldDescriptor, Object> fieldPair : message.getAllFields()
        .entrySet()) {
      FieldDescriptor field = fieldPair.getKey();
      writer.name(getFieldName(field));
      if (field.isRepeated()) {
        writer.beginArray();
        for (Object value : (List<?>) fieldPair.getValue()) {
          printFieldValue(field.getType(), value, writer);
        }
        writer.endArray();
      } else {
        printFieldValue(field.getType(), fieldPair.getValue(), writer);
      }
    }
    printUnknownFieldValues(message.getUnknownFields(), writer);
    writer.endObject();
  }

  public static void stringify(Message message, Writer output)
      throws IOException {
    JsonWriter writer = new JsonWriter(output);
    stringify(message, writer);
    writer.flush();
    writer.close();
  }

  public static String stringify(Message message, boolean inlined) {
    StringWriter buffer = new StringWriter(1024);
    try {
      JsonWriter writer = new JsonWriter(buffer);
      if (!inlined) {
        writer.setIndent("  ");
      }
      stringify(message, writer);
      writer.flush();
      writer.close();
    } catch (IOException e) {
    }
    return buffer.toString();
  }

  public static String stringify(Message message) {
    return stringify(message, false);
  }

  private static FieldDescriptor getField(Descriptor type, String name)
      throws TextFormat.ParseException {
    FieldDescriptor field = type.findFieldByName(name);
    // Group names are expected to be capitalized as they appear in the
    // .proto file, which actually matches their type names, not their field
    // names.
    if (field == null) {
      // Explicitly specify US locale so that this code does not break when
      // executing in Turkey.
      String lowerName = name.toLowerCase(Locale.US);
      field = type.findFieldByName(lowerName);
      // If the case-insensitive match worked but the field is NOT a group,
      if (field != null && field.getType() != FieldDescriptor.Type.GROUP) {
        field = null;
      }
    }
    // Again, special-case group names as described above.
    if (field != null && field.getType() == FieldDescriptor.Type.GROUP
        && !field.getMessageType().getName().equals(name)) {
      field = null;
    }
    if (field == null) {
      throw new TextFormat.ParseException("Message type \""
          + type.getFullName() + "\" has no field named \"" + name + "\".");
    }
    return field;
  }

  private static Object readValue(JsonReader reader, Message.Builder builder,
      FieldDescriptor field, ExtensionRegistry registry,
      ExtensionRegistry.ExtensionInfo extension) throws IOException {
    switch (field.getType()) {
    case INT32:
    case SINT32:
    case SFIXED32:
      return reader.nextInt();
    case INT64:
    case SINT64:
    case SFIXED64:
      return reader.nextLong();
    case UINT32:
    case FIXED32:
      return reader.nextInt();
    case UINT64:
    case FIXED64:
      return reader.nextLong();
    case FLOAT:
      return (float) reader.nextDouble();
    case DOUBLE:
      return reader.nextDouble();
    case BOOL:
      return reader.nextBoolean();
    case STRING:
      return reader.nextString();
    case BYTES:
      return Base64.decode(reader.nextString());
    case ENUM:
      final EnumDescriptor enumType = field.getEnumType();
      String enumName = reader.nextString();
      Object value = enumType.findValueByName(enumName);
      if (value == null) {
        throw new TextFormat.ParseException("Enum type \""
            + enumType.getFullName() + "\" has no value named \"" + enumName
            + "\".");
      }
      return value;
    case MESSAGE:
    case GROUP:
      Message.Builder subBuilder;
      if (extension == null) {
        subBuilder = builder.newBuilderForField(field);
      } else {
        subBuilder = extension.defaultInstance.newBuilderForType();
      }
      parse(reader, registry, subBuilder);
      return subBuilder.build();
    }
    throw new TextFormat.ParseException("Invalid type \"" + field.getType()
        + "\"");
  }

  public static void parse(JsonReader reader, ExtensionRegistry registry,
      Message.Builder builder) throws IOException {
    Descriptor type = builder.getDescriptorForType();
    reader.beginObject();
    while (reader.hasNext()) {
      FieldDescriptor field;
      ExtensionRegistry.ExtensionInfo extension = null;
      String name = reader.nextName();
      if (name.startsWith("(") && name.endsWith(")")) {
        name = name.substring(1, name.length() - 2);
        extension = registry.findExtensionByName(name);
        if (extension == null) {
          throw new TextFormat.ParseException("Extension \"" + name
              + "\" not found in the ExtensionRegistry.");
        } else if (extension.descriptor.getContainingType() != type) {
          throw new TextFormat.ParseException("Extension \"" + name
              + "\" does not extend message type \"" + type.getFullName()
              + "\".");
        }
        field = extension.descriptor;
      } else {
        field = getField(type, name);
      }
      Object value = readValue(reader, builder, field, registry, extension);
      if (field.isRepeated()) {
        builder.addRepeatedField(field, value);
      } else {
        builder.setField(field, value);
      }
    }
    reader.endObject();
  }
}
